/*

 * Licensed to the Apache Software Foundation (ASF) under one

 * or more contributor license agreements.  See the NOTICE file

 * distributed with this work for additional information

 * regarding copyright ownership.  The ASF licenses this file

 * to you under the Apache License, Version 2.0 (the

 * "License"); you may not use this file except in compliance

 * with the License.  You may obtain a copy of the License at

 *

 *     http://www.apache.org/licenses/LICENSE-2.0

 *

 * Unless required by applicable law or agreed to in writing, software

 * distributed under the License is distributed on an "AS IS" BASIS,

 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

 * See the License for the specific language governing permissions and

 * limitations under the License.

 */

package com.bff.gaia.unified.sdk.util;



import com.bff.gaia.unified.vendor.guava.com.google.common.base.Function;

import com.bff.gaia.unified.vendor.guava.com.google.common.base.Joiner;

import com.bff.gaia.unified.vendor.guava.com.google.common.base.Predicate;

import com.bff.gaia.unified.vendor.guava.com.google.common.collect.*;

import com.bff.gaia.unified.vendor.guava.com.google.common.reflect.Invokable;

import com.bff.gaia.unified.vendor.guava.com.google.common.reflect.Parameter;

import com.bff.gaia.unified.vendor.guava.com.google.common.reflect.TypeToken;

import org.hamcrest.Description;

import org.hamcrest.Matcher;

import org.hamcrest.StringDescription;

import org.hamcrest.TypeSafeDiagnosingMatcher;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;



import java.io.IOException;

import java.lang.annotation.Annotation;

import java.lang.reflect.*;

import java.util.Collection;

import java.util.Collections;

import java.util.List;

import java.util.Set;

import java.util.regex.Pattern;



import static org.hamcrest.Matchers.anyOf;



/**

 * Represents the API surface of a package prefix. Used for accessing public classes, methods, and

 * the types they reference, to control what dependencies are re-exported.

 *

 * <p>For the purposes of calculating the public API surface, exposure includes any public or

 * protected occurrence of:

 *

 * <ul>

 *   <li>superclasses

 *   <li>interfaces implemented

 *   <li>actual type arguments to generic types

 *   <li>array component types

 *   <li>method return types

 *   <li>method parameter types

 *   <li>type variable bounds

 *   <li>wildcard bounds

 * </ul>

 *

 * <p>Exposure is a transitive property. The resulting map excludes primitives and array classes

 * themselves.

 *

 * <p>It is prudent (though not required) to prune prefixes like "java" via the builder method

 * {@link #pruningPrefix} to halt the traversal so it does not uselessly catalog references that are

 * not interesting.

 */

@SuppressWarnings("rawtypes")

public class ApiSurface {

  private static final Logger LOG = LoggerFactory.getLogger(ApiSurface.class);



  /** A factory method to create a {@link Class} matcher for classes residing in a given package. */

  public static Matcher<Class<?>> classesInPackage(final String packageName) {

    return new Matchers.ClassInPackage(packageName);

  }



  /**

   * A factory method to create an {@link ApiSurface} matcher, producing a positive match if the

   * queried api surface contains ONLY classes described by the provided matchers.

   */

  public static Matcher<ApiSurface> containsOnlyClassesMatching(

      final Set<Matcher<Class<?>>> classMatchers) {

    return new Matchers.ClassesInSurfaceMatcher(classMatchers);

  }



  /** See {@link ApiSurface#containsOnlyClassesMatching(Set)}. */

  @SafeVarargs

  public static Matcher<ApiSurface> containsOnlyClassesMatching(

      final Matcher<Class<?>>... classMatchers) {

    return new Matchers.ClassesInSurfaceMatcher(Sets.newHashSet(classMatchers));

  }



  /** See {@link ApiSurface#containsOnlyPackages(Set)}. */

  public static Matcher<ApiSurface> containsOnlyPackages(final String... packageNames) {

    return containsOnlyPackages(Sets.newHashSet(packageNames));

  }



  /**

   * A factory method to create an {@link ApiSurface} matcher, producing a positive match if the

   * queried api surface contains classes ONLY from specified package names.

   */

  public static Matcher<ApiSurface> containsOnlyPackages(final Set<String> packageNames) {



    final Function<String, Matcher<Class<?>>> packageNameToClassMatcher =

        ApiSurface::classesInPackage;



    final ImmutableSet<Matcher<Class<?>>> classesInPackages =

        FluentIterable.from(packageNames).transform(packageNameToClassMatcher).toSet();



    return containsOnlyClassesMatching(classesInPackages);

  }



  /**

   * {@link Matcher}s for use in {@link ApiSurface} related tests that aim to keep the public API

   * conformant to a hard-coded policy by controlling what classes are allowed to be exposed by an

   * API surface.

   */

  // based on previous code by @kennknowles and others.

  private static class Matchers {



    private static class ClassInPackage extends TypeSafeDiagnosingMatcher<Class<?>> {



      private final String packageName;



      private ClassInPackage(final String packageName) {

        this.packageName = packageName;

      }



      @Override

      public void describeTo(final Description description) {

        description.appendText("Classes in package \"");

        description.appendText(packageName);

        description.appendText("\"");

      }



      @Override

      protected boolean matchesSafely(final Class<?> clazz, final Description mismatchDescription) {

        return clazz.getName().startsWith(packageName + ".");

      }

    }



    private static class ClassesInSurfaceMatcher extends TypeSafeDiagnosingMatcher<ApiSurface> {



      private final Set<Matcher<Class<?>>> classMatchers;



      private ClassesInSurfaceMatcher(final Set<Matcher<Class<?>>> classMatchers) {

        this.classMatchers = classMatchers;

      }



      private boolean verifyNoAbandoned(

          final ApiSurface checkedApiSurface,

          final Set<Matcher<Class<?>>> allowedClasses,

          final Description mismatchDescription) {



        // <helper_lambdas>



        final Function<Matcher<Class<?>>, String> toMessage =

            abandonedClassMacther -> {

              final StringDescription description = new StringDescription();

              description.appendText("No ");

              abandonedClassMacther.describeTo(description);

              return description.toString();

            };



        final Predicate<Matcher<Class<?>>> matchedByExposedClasses =

            classMatcher ->

                FluentIterable.from(checkedApiSurface.getExposedClasses())

                    .anyMatch(classMatcher::matches);



        // </helper_lambdas>



        final ImmutableSet<Matcher<Class<?>>> matchedClassMatchers =

            FluentIterable.from(allowedClasses).filter(matchedByExposedClasses).toSet();



        final Sets.SetView<Matcher<Class<?>>> abandonedClassMatchers =

            Sets.difference(allowedClasses, matchedClassMatchers);



        final ImmutableList<String> messages =

            FluentIterable.from(abandonedClassMatchers)

                .transform(toMessage)

                .toSortedList(Ordering.natural());



        if (!messages.isEmpty()) {

          mismatchDescription.appendText(

              "The following white-listed scopes did not have matching classes on the API surface:"

                  + "\n\t"

                  + Joiner.on("\n\t").join(messages));

        }



        return messages.isEmpty();

      }



      private boolean verifyNoDisallowed(

          final ApiSurface checkedApiSurface,

          final Set<Matcher<Class<?>>> allowedClasses,

          final Description mismatchDescription) {



        /* <helper_lambdas> */



        final Function<Class<?>, List<Class<?>>> toExposure = checkedApiSurface::getAnyExposurePath;



        final Maps.EntryTransformer<Class<?>, List<Class<?>>, String> toMessage =

            (aClass, exposure) ->

                aClass + " exposed via:\n\t\t" + Joiner.on("\n\t\t").join(exposure);



        final Predicate<Class<?>> disallowed = aClass -> !classIsAllowed(aClass, allowedClasses);



        /* </helper_lambdas> */



        final FluentIterable<Class<?>> disallowedClasses =

            FluentIterable.from(checkedApiSurface.getExposedClasses()).filter(disallowed);



        final ImmutableMap<Class<?>, List<Class<?>>> exposures =

            Maps.toMap(disallowedClasses, toExposure);



        final ImmutableList<String> messages =

            FluentIterable.from(Maps.transformEntries(exposures, toMessage).values())

                .toSortedList(Ordering.natural());



        if (!messages.isEmpty()) {

          mismatchDescription.appendText(

              "The following disallowed classes appeared on the API surface:\n\t"

                  + Joiner.on("\n\t").join(messages));

        }



        return messages.isEmpty();

      }



      @SuppressWarnings({"rawtypes", "unchecked"})

      private boolean classIsAllowed(

          final Class<?> clazz, final Set<Matcher<Class<?>>> allowedClasses) {

        // Safe cast inexpressible in Java without rawtypes

        return anyOf((Iterable) allowedClasses).matches(clazz);

      }



      @Override

      protected boolean matchesSafely(

		  final ApiSurface apiSurface, final Description mismatchDescription) {

        final boolean noDisallowed =

            verifyNoDisallowed(apiSurface, classMatchers, mismatchDescription);



        final boolean noAbandoned =

            verifyNoAbandoned(apiSurface, classMatchers, mismatchDescription);



        return noDisallowed && noAbandoned;

      }



      @Override

      public void describeTo(final Description description) {

        description.appendText("API surface to include only:" + "\n\t");

        for (final Matcher<Class<?>> classMatcher : classMatchers) {

          classMatcher.describeTo(description);

          description.appendText("\n\t");

        }

      }

    }

  }



  ///////////////



  /** Returns an empty {@link ApiSurface}. */

  public static ApiSurface empty() {

    LOG.debug("Returning an empty ApiSurface");

    return new ApiSurface(Collections.emptySet(), Collections.emptySet());

  }



  /** Returns an {@link ApiSurface} object representing the given package and all subpackages. */

  public static ApiSurface ofPackage(String packageName, ClassLoader classLoader)

      throws IOException {

    return ApiSurface.empty().includingPackage(packageName, classLoader);

  }



  /** Returns an {@link ApiSurface} object representing the given package and all subpackages. */

  public static ApiSurface ofPackage(Package aPackage, ClassLoader classLoader) throws IOException {

    return ofPackage(aPackage.getName(), classLoader);

  }



  /** Returns an {@link ApiSurface} object representing just the surface of the given class. */

  public static ApiSurface ofClass(Class<?> clazz) {

    return ApiSurface.empty().includingClass(clazz);

  }



  /**

   * Returns an {@link ApiSurface} like this one, but also including the named package and all of

   * its subpackages.

   */

  public ApiSurface includingPackage(String packageName, ClassLoader classLoader)

      throws IOException {

    ClassPath classPath = ClassPath.from(classLoader);



    Set<Class<?>> newRootClasses = Sets.newHashSet();

    for (ClassPath.ClassInfo classInfo : classPath.getTopLevelClassesRecursive(packageName)) {

      Class clazz = null;

      try {

        clazz = classInfo.load();

      } catch (NoClassDefFoundError e) {

        // TODO: Ignore any NoClassDefFoundError errors as a workaround. (BEAM-2231)

        LOG.warn("Failed to load class: {}", classInfo.toString(), e);

        continue;

      }



      if (exposed(clazz.getModifiers())) {

        newRootClasses.add(clazz);

      }

    }

    LOG.debug("Including package {} and subpackages: {}", packageName, newRootClasses);

    newRootClasses.addAll(rootClasses);



    return new ApiSurface(newRootClasses, patternsToPrune);

  }



  /** Returns an {@link ApiSurface} like this one, but also including the given class. */

  public ApiSurface includingClass(Class<?> clazz) {

    Set<Class<?>> newRootClasses = Sets.newHashSet();

    LOG.debug("Including class {}", clazz);

    newRootClasses.add(clazz);

    newRootClasses.addAll(rootClasses);

    return new ApiSurface(newRootClasses, patternsToPrune);

  }



  /**

   * Returns an {@link ApiSurface} like this one, but pruning transitive references from classes

   * whose full name (including package) begins with the provided prefix.

   */

  public ApiSurface pruningPrefix(String prefix) {

    return pruningPattern(Pattern.compile(Pattern.quote(prefix) + ".*"));

  }



  /** Returns an {@link ApiSurface} like this one, but pruning references from the named class. */

  public ApiSurface pruningClassName(String className) {

    return pruningPattern(Pattern.compile(Pattern.quote(className)));

  }



  /**

   * Returns an {@link ApiSurface} like this one, but pruning references from the provided class.

   */

  public ApiSurface pruningClass(Class<?> clazz) {

    return pruningClassName(clazz.getName());

  }



  /**

   * Returns an {@link ApiSurface} like this one, but pruning transitive references from classes

   * whose full name (including package) begins with the provided prefix.

   */

  public ApiSurface pruningPattern(Pattern pattern) {

    Set<Pattern> newPatterns = Sets.newHashSet();

    newPatterns.addAll(patternsToPrune);

    newPatterns.add(pattern);

    return new ApiSurface(rootClasses, newPatterns);

  }



  /** See {@link #pruningPattern(Pattern)}. */

  public ApiSurface pruningPattern(String patternString) {

    return pruningPattern(Pattern.compile(patternString));

  }



  /** Returns all public classes originally belonging to the package in the {@link ApiSurface}. */

  public Set<Class<?>> getRootClasses() {

    return rootClasses;

  }



  /** Returns exposed types in this set, including arrays and primitives as specified. */

  public Set<Class<?>> getExposedClasses() {

    return getExposedToExposers().keySet();

  }



  /**

   * Returns a path from an exposed class to a root class. There may be many, but this gives only

   * one.

   *

   * <p>If there are only cycles, with no path back to a root class, throws IllegalStateException.

   */

  public List<Class<?>> getAnyExposurePath(Class<?> exposedClass) {

    Set<Class<?>> excluded = Sets.newHashSet();

    excluded.add(exposedClass);

    List<Class<?>> path = getAnyExposurePath(exposedClass, excluded);

    if (path == null) {

      throw new IllegalArgumentException(

          "Class "

              + exposedClass

              + " has no path back to any root class."

              + " It should never have been considered exposed.");

    } else {

      return path;

    }

  }



  /**

   * Returns a path from an exposed class to a root class. There may be many, but this gives only

   * one. It will not return a path that crosses the excluded classes.

   *

   * <p>If there are only cycles or paths through the excluded classes, returns null.

   *

   * <p>If the class is not actually in the exposure map, throws IllegalArgumentException

   */

  private List<Class<?>> getAnyExposurePath(Class<?> exposedClass, Set<Class<?>> excluded) {

    List<Class<?>> exposurePath = Lists.newArrayList();

    exposurePath.add(exposedClass);



    Collection<Class<?>> exposers = getExposedToExposers().get(exposedClass);

    if (exposers.isEmpty()) {

      throw new IllegalArgumentException("Class " + exposedClass + " is not exposed.");

    }



    for (Class<?> exposer : exposers) {

      if (excluded.contains(exposer)) {

        continue;

      }



      // A null exposer means this is already a root class.

      if (exposer == null) {

        return exposurePath;

      }



      List<Class<?>> restOfPath =

          getAnyExposurePath(exposer, Sets.union(excluded, Sets.newHashSet(exposer)));



      if (restOfPath != null) {

        exposurePath.addAll(restOfPath);

        return exposurePath;

      }

    }

    return null;

  }



  ////////////////////////////////////////////////////////////////////



  // Fields initialized upon construction

  private final Set<Class<?>> rootClasses;

  private final Set<Pattern> patternsToPrune;



  // Fields computed on-demand

  private Multimap<Class<?>, Class<?>> exposedToExposers = null;

  private Pattern prunedPattern = null;

  private Set<Type> visited = null;



  private ApiSurface(Set<Class<?>> rootClasses, Set<Pattern> patternsToPrune) {

    this.rootClasses = rootClasses;

    this.patternsToPrune = patternsToPrune;

  }



  /**

   * A map from exposed types to place where they are exposed, in the sense of being a part of a

   * public-facing API surface.

   *

   * <p>This map is the adjencency list representation of a directed graph, where an edge from type

   * {@code T1} to type {@code T2} indicates that {@code T2} directly exposes {@code T1} in its API

   * surface.

   *

   * <p>The traversal methods in this class are designed to avoid repeatedly processing types, since

   * there will almost always be cyclic references.

   */

  private Multimap<Class<?>, Class<?>> getExposedToExposers() {

    if (exposedToExposers == null) {

      constructExposedToExposers();

    }

    return exposedToExposers;

  }



  /** See {@link #getExposedToExposers}. */

  private void constructExposedToExposers() {

    visited = Sets.newHashSet();

    exposedToExposers =

        Multimaps.newSetMultimap(

            Maps.<Class<?>, Collection<Class<?>>>newHashMap(), Sets::newHashSet);



    for (Class<?> clazz : rootClasses) {

      addExposedTypes(clazz, null);

    }

  }



  /** A combined {@code Pattern} that implements all the pruning specified. */

  private Pattern getPrunedPattern() {

    if (prunedPattern == null) {

      constructPrunedPattern();

    }

    return prunedPattern;

  }



  /** See {@link #getPrunedPattern}. */

  private void constructPrunedPattern() {

    Set<String> prunedPatternStrings = Sets.newHashSet();

    for (Pattern patternToPrune : patternsToPrune) {

      prunedPatternStrings.add(patternToPrune.pattern());

    }

    prunedPattern = Pattern.compile("(" + Joiner.on(")|(").join(prunedPatternStrings) + ")");

  }



  /** Whether a type and all that it references should be pruned from the graph. */

  private boolean pruned(Type type) {

    return pruned(TypeToken.of(type).getRawType());

  }



  /** Whether a class and all that it references should be pruned from the graph. */

  private boolean pruned(Class<?> clazz) {

    return clazz.isPrimitive()

        || clazz.isArray()

        || getPrunedPattern().matcher(clazz.getName()).matches();

  }



  /** Whether a type has already beens sufficiently processed. */

  private boolean done(Type type) {

    return visited.contains(type);

  }



  private void recordExposure(Class<?> exposed, Class<?> cause) {

    exposedToExposers.put(exposed, cause);

  }



  private void recordExposure(Type exposed, Class<?> cause) {

    exposedToExposers.put(TypeToken.of(exposed).getRawType(), cause);

  }



  private void visit(Type type) {

    visited.add(type);

  }



  /** See {@link #addExposedTypes(Type, Class)}. */

  private void addExposedTypes(TypeToken type, Class<?> cause) {

    LOG.debug(

        "Adding exposed types from {}, which is the type in type token {}", type.getType(), type);

    addExposedTypes(type.getType(), cause);

  }



  /**

   * Adds any references learned by following a link from {@code cause} to {@code type}. This will

   * dispatch according to the concrete {@code Type} implementation. See the other overloads of

   * {@code addExposedTypes} for their details.

   */

  private void addExposedTypes(Type type, Class<?> cause) {

    if (type instanceof TypeVariable) {

      LOG.debug("Adding exposed types from {}, which is a type variable", type);

      addExposedTypes((TypeVariable) type, cause);

    } else if (type instanceof WildcardType) {

      LOG.debug("Adding exposed types from {}, which is a wildcard type", type);

      addExposedTypes((WildcardType) type, cause);

    } else if (type instanceof GenericArrayType) {

      LOG.debug("Adding exposed types from {}, which is a generic array type", type);

      addExposedTypes((GenericArrayType) type, cause);

    } else if (type instanceof ParameterizedType) {

      LOG.debug("Adding exposed types from {}, which is a parameterized type", type);

      addExposedTypes((ParameterizedType) type, cause);

    } else if (type instanceof Class) {

      LOG.debug("Adding exposed types from {}, which is a class", type);

      addExposedTypes((Class) type, cause);

    } else {

      throw new IllegalArgumentException("Unknown implementation of Type");

    }

  }



  /**

   * Adds any types exposed to this set. These will come from the (possibly absent) bounds on the

   * type variable.

   */

  private void addExposedTypes(TypeVariable type, Class<?> cause) {

    if (done(type)) {

      return;

    }

    visit(type);

    for (Type bound : type.getBounds()) {

      LOG.debug("Adding exposed types from {}, which is a type bound on {}", bound, type);

      addExposedTypes(bound, cause);

    }

  }



  /**

   * Adds any types exposed to this set. These will come from the (possibly absent) bounds on the

   * wildcard.

   */

  private void addExposedTypes(WildcardType type, Class<?> cause) {

    visit(type);

    for (Type lowerBound : type.getLowerBounds()) {

      LOG.debug(

          "Adding exposed types from {}, which is a type lower bound on wildcard type {}",

          lowerBound,

          type);

      addExposedTypes(lowerBound, cause);

    }

    for (Type upperBound : type.getUpperBounds()) {

      LOG.debug(

          "Adding exposed types from {}, which is a type upper bound on wildcard type {}",

          upperBound,

          type);

      addExposedTypes(upperBound, cause);

    }

  }



  /**

   * Adds any types exposed from the given array type. The array type itself is not added. The cause

   * of the exposure of the underlying type is considered whatever type exposed the array type.

   */

  private void addExposedTypes(GenericArrayType type, Class<?> cause) {

    if (done(type)) {

      return;

    }

    visit(type);

    LOG.debug(

        "Adding exposed types from {}, which is the component type on generic array type {}",

        type.getGenericComponentType(),

        type);

    addExposedTypes(type.getGenericComponentType(), cause);

  }



  /**

   * Adds any types exposed to this set. Even if the root type is to be pruned, the actual type

   * arguments are processed.

   */

  private void addExposedTypes(ParameterizedType type, Class<?> cause) {

    // Even if the type is already done, this link to it may be new

    boolean alreadyDone = done(type);

    if (!pruned(type)) {

      visit(type);

      recordExposure(type, cause);

    }

    if (alreadyDone) {

      return;

    }



    // For a parameterized type, pruning does not take place

    // here, only for the raw class.

    // The type parameters themselves may not be pruned,

    // for example with List<MyApiType> probably the

    // standard List is pruned, but MyApiType is not.

    LOG.debug(

        "Adding exposed types from {}, which is the raw type on parameterized type {}",

        type.getRawType(),

        type);

    addExposedTypes(type.getRawType(), cause);

    for (Type typeArg : type.getActualTypeArguments()) {

      LOG.debug(

          "Adding exposed types from {}, which is a type argument on parameterized type {}",

          typeArg,

          type);

      addExposedTypes(typeArg, cause);

    }

  }



  /**

   * Adds a class and all of the types it exposes. The cause of the class being exposed is given,

   * and the cause of everything within the class is that class itself.

   */

  private void addExposedTypes(Class<?> clazz, Class<?> cause) {

    if (pruned(clazz)) {

      return;

    }

    // Even if `clazz` has been visited, the link from `cause` may be new

    boolean alreadyDone = done(clazz);

    visit(clazz);

    recordExposure(clazz, cause);

    if (alreadyDone || pruned(clazz)) {

      return;

    }



    TypeToken<?> token = TypeToken.of(clazz);

    for (TypeToken<?> superType : token.getTypes()) {

      if (!superType.equals(token)) {

        LOG.debug(

            "Adding exposed types from {}, which is a super type token on {}", superType, clazz);

        addExposedTypes(superType, clazz);

      }

    }

    for (Class innerClass : clazz.getDeclaredClasses()) {

      if (exposed(innerClass.getModifiers())) {

        LOG.debug(

            "Adding exposed types from {}, which is an exposed inner class of {}",

            innerClass,

            clazz);

        addExposedTypes(innerClass, clazz);

      }

    }

    for (Field field : clazz.getDeclaredFields()) {

      if (exposed(field.getModifiers())) {

        LOG.debug("Adding exposed types from {}, which is an exposed field on {}", field, clazz);

        addExposedTypes(field, clazz);

      }

    }

    for (Invokable invokable : getExposedInvokables(token)) {

      LOG.debug(

          "Adding exposed types from {}, which is an exposed invokable on {}", invokable, clazz);

      addExposedTypes(invokable, clazz);

    }

  }



  private void addExposedTypes(Invokable<?, ?> invokable, Class<?> cause) {

    addExposedTypes(invokable.getReturnType(), cause);

    for (Annotation annotation : invokable.getAnnotations()) {

      LOG.debug(

          "Adding exposed types from {}, which is an annotation on invokable {}",

          annotation,

          invokable);

      addExposedTypes(annotation.annotationType(), cause);

    }

    for (Parameter parameter : invokable.getParameters()) {

      LOG.debug(

          "Adding exposed types from {}, which is a parameter on invokable {}",

          parameter,

          invokable);

      addExposedTypes(parameter, cause);

    }

    for (TypeToken<?> exceptionType : invokable.getExceptionTypes()) {

      LOG.debug(

          "Adding exposed types from {}, which is an exception type on invokable {}",

          exceptionType,

          invokable);

      addExposedTypes(exceptionType, cause);

    }

  }



  private void addExposedTypes(Parameter parameter, Class<?> cause) {

    LOG.debug(

        "Adding exposed types from {}, which is the type of parameter {}",

        parameter.getType(),

        parameter);

    addExposedTypes(parameter.getType(), cause);

    for (Annotation annotation : parameter.getAnnotations()) {

      LOG.debug(

          "Adding exposed types from {}, which is an annotation on parameter {}",

          annotation,

          parameter);

      addExposedTypes(annotation.annotationType(), cause);

    }

  }



  private void addExposedTypes(Field field, Class<?> cause) {

    addExposedTypes(field.getGenericType(), cause);

    for (Annotation annotation : field.getDeclaredAnnotations()) {

      LOG.debug(

          "Adding exposed types from {}, which is an annotation on field {}", annotation, field);

      addExposedTypes(annotation.annotationType(), cause);

    }

  }



  /** Returns an {@link Invokable} for each public methods or constructors of a type. */

  private Set<Invokable> getExposedInvokables(TypeToken<?> type) {

    Set<Invokable> invokables = Sets.newHashSet();



    for (Constructor constructor : type.getRawType().getConstructors()) {

      if (0 != (constructor.getModifiers() & (Modifier.PUBLIC | Modifier.PROTECTED))) {

        invokables.add(type.constructor(constructor));

      }

    }



    for (Method method : type.getRawType().getMethods()) {

      if (0 != (method.getModifiers() & (Modifier.PUBLIC | Modifier.PROTECTED))) {

        invokables.add(type.method(method));

      }

    }



    return invokables;

  }



  /** Returns true of the given modifier bitmap indicates exposure (public or protected access). */

  private boolean exposed(int modifiers) {

    return 0 != (modifiers & (Modifier.PUBLIC | Modifier.PROTECTED));

  }



  ////////////////////////////////////////////////////////////////////////////



}